feat(stats): wire token-savings telemetry into search and find_related - #82
Conversation
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 26 |
| Duplication | 10 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
No issues found across 5 files
Architecture diagram
sequenceDiagram
participant CLI as CLI Dispatch
participant MCP as MCP Server
participant Stats as save_search_stats()
participant Index as CspIndex
participant Disk as Source Tree on Disk
participant File as ~/.csp/savings.jsonl
Note over CLI,MCP: Search / find-related entry points
CLI->>Index: search(query, options)
Index->>Disk: read_from_path(root)
Disk-->>Index: file_sizes (UTF-16 char counts)
Index-->>CLI: results
CLI->>Stats: save_search_stats(stats_file, results, CallType::Search, index.file_sizes, max_snippet_lines)
alt max_snippet_lines is None
Stats->>Stats: snippet_chars = utf16_len(content) (full chunk)
else max_snippet_lines is Some(0)
Stats->>Stats: snippet_chars = 0
else max_snippet_lines is Some(n)
Stats->>Stats: snippet_chars = utf16_len(first n lines)
end
Stats->>File: Append JSONL record
File-->>Stats: (swallow I/O errors)
Note over MCP,Index: MCP tools with injected stats_file
MCP->>Index: search_tool(cache, ..., stats_file)
Index-->>MCP: results
alt stats_file is Some
MCP->>Stats: save_search_stats(stats_file, results, CallType::Search, index.file_sizes, max_snippet_lines)
Stats->>File: Append JSONL record
end
Note over Index,Disk: File sizes lifecycle
alt from_path() (local directory)
Index->>Disk: compute_file_sizes(root, chunks)
Disk-->>Index: HashMap<path, utf16_char_count>
else from_git() (remote URL)
Index->>Index: clone from_path computes sizes
Index->>Index: clone file_sizes before re-rooting
else load_from_disk() (cached manifest)
alt root is a present local directory
Index->>Disk: compute_file_sizes(root, chunks)
Disk-->>Index: HashMap<path, utf16_char_count>
else root is git URL or missing
Index->>Index: file_sizes = empty (file_chars = 0)
end
end
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Code Review
이번 풀 리퀘스트는 검색 및 관련 청크 찾기 기능에 토큰 절약 텔레메트리(통계 기록) 기능을 추가하는 변경사항을 담고 있습니다. 인덱스 빌드 시 파일 크기를 캡처하고, 검색 결과 출력 시 실제 전달된 문자 수를 계산하여 통계 파일에 기록하도록 구현되었습니다. 코드 리뷰 결과, compute_file_sizes 함수에서 상위 디렉터리 참조(..)나 절대 경로를 검증하지 않아 발생할 수 있는 경로 탐색(Path Traversal) 취약점(HIGH)과, delivered_chars 함수에서 불필요한 메모리 할당으로 인한 성능 저하 우려(MEDIUM)가 지적되었습니다. 두 피드백 모두 타당하며 제시된 코드로 수정할 것을 권장합니다.
|
Greptile SummaryThis PR wires the previously inert
Confidence Score: 4/5Safe to merge; the only concrete issue is that The core telemetry wiring, the
Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant User
participant CLI as csp CLI / MCP Tool
participant dispatch as dispatch_with_stats(stats_file)
participant search as search_output / find_related_output
participant index as CspIndex
participant stats as save_search_stats
participant file as savings.jsonl
User->>CLI: csp search "query"
CLI->>dispatch: "dispatch_with_stats(Search{...}, stats_file)"
dispatch->>index: load_index() → CspIndex (with file_sizes)
index-->>dispatch: "CspIndex {chunks, file_sizes}"
dispatch->>search: "search_output(&idx, query, top_k, max_lines, Some(stats_file))"
search->>index: index.search(query, options)
index-->>search: "Vec<SearchResult>"
search->>stats: save_search_stats(stats_file, results, Search, file_sizes, max_lines)
stats->>stats: compute snippet_chars via delivered_chars()
stats->>stats: dedup file paths → sum file_chars
stats->>file: append JSONL record
search-->>dispatch: JSON string
dispatch-->>User: stdout
Note over index: file_sizes populated at from_path() / from_git() / load_from_disk()
Note over stats: Best-effort: I/O errors are swallowed
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant User
participant CLI as csp CLI / MCP Tool
participant dispatch as dispatch_with_stats(stats_file)
participant search as search_output / find_related_output
participant index as CspIndex
participant stats as save_search_stats
participant file as savings.jsonl
User->>CLI: csp search "query"
CLI->>dispatch: "dispatch_with_stats(Search{...}, stats_file)"
dispatch->>index: load_index() → CspIndex (with file_sizes)
index-->>dispatch: "CspIndex {chunks, file_sizes}"
dispatch->>search: "search_output(&idx, query, top_k, max_lines, Some(stats_file))"
search->>index: index.search(query, options)
index-->>search: "Vec<SearchResult>"
search->>stats: save_search_stats(stats_file, results, Search, file_sizes, max_lines)
stats->>stats: compute snippet_chars via delivered_chars()
stats->>stats: dedup file paths → sum file_chars
stats->>file: append JSONL record
search-->>dispatch: JSON string
dispatch-->>User: stdout
Note over index: file_sizes populated at from_path() / from_git() / load_from_disk()
Note over stats: Best-effort: I/O errors are swallowed
|
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces the max_snippet_lines option to limit the number of source code lines returned per search or find-related result, defaulting to 10 lines to optimize token usage. It also adds token-savings telemetry to track and record the characters delivered to the caller. The review feedback correctly identifies potential integer truncation risks when casting i64 to usize in the resolve_snippet_lines helper functions across both main.rs and mcp_server.rs, suggesting a safer conversion using try_from.
`save_search_stats` was defined and tested but never called — no code path recorded to `~/.csp/savings.jsonl`, so `csp savings` always reported zero. Wire it in at the CLI/MCP app boundary (keeping `CspIndex::search` a pure, side-effect-free library call) and apply the semble#206 snippet accounting. - CspIndex gains `file_sizes` (repo-relative path → UTF-16 char count), captured at build time in `from_path`, carried through `from_git`'s re-root, and recomputed in `load_from_disk` when the source root is a still-present local directory (mirrors semble reading sizes off `root`). - `save_search_stats` takes `max_snippet_lines` so recorded `snippet_chars` reflects what the caller actually received (semble#206): `None` → full chunk, `Some(0)` → 0, `Some(n)` → first n lines. - Recording happens in `search_output` / `find_related_output` (CLI) and `search_tool` / `find_related_tool` (MCP), each behind an injected `Option<&Path>` stats file so tests redirect telemetry off the real `~/.csp/savings.jsonl`. The MCP server and CLI dispatch default to the real file; tests pass a temp file or `None`. Closes #81.
- guard compute_file_sizes against absolute or parent-escaping chunk paths from a tampered on-disk index - compute delivered_chars without materializing the joined snippet
8318050 to
c94908a
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces token-savings telemetry to the search and find-related operations by tracking file sizes and recording search statistics. The review feedback highlights a high-severity security risk in compute_file_sizes where symlinks or special files are not validated before reading, which could lead to a Denial of Service. Additionally, storing relative root paths in CspIndex::from_path may cause incorrect file size computations when loaded from different directories, and the search_tool function violates the style guide by exceeding the limit of five parameters.
- store from_path root as an absolute path (upstream path.resolve() parity) - read only regular files in compute_file_sizes (skip symlinks, dirs, FIFOs) - document the too_many_arguments allow on search_tool like find_related_tool
|
/gemini review |
|
There was a problem hiding this comment.
Code Review
This pull request introduces token-savings telemetry to track and record character savings during search and find-related operations. It adds a file_sizes map to CspIndex to store per-file character counts, computes these sizes during index creation or load, and records telemetry via save_search_stats while respecting snippet line limits. Feedback on the changes highlights a performance concern where eagerly computing file sizes for all chunks during index load introduces significant disk I/O overhead, and suggests making this computation lazy.
…y) into incremental reindexing
…vectors, and BM25 postings (#91) * feat(index): incremental reindexing — reuse unchanged files' chunks, vectors, and BM25 postings Port upstream semble #225 (partial reindexing) to the Rust core. When the cached index's whole-tree content hash is stale, `load_or_build_index` now seeds the rebuild with the previous index instead of rebuilding from scratch: files whose per-file content hash is unchanged keep their chunks, vector rows, and BM25 postings; only changed files are re-chunked and re-embedded, and deleted files' postings are dropped. - `indexing/types.rs`: `FileManifestEntry {hash, start, count}`, `PreviousIndex::try_new` (alignment checks), `make_chunk_id`. - `sparse.rs`: `Bm25Index` becomes the id-keyed incremental index from upstream `bm25.py` (`add_document` / `remove_document` / `set_doc_order`); `bm25.json` v2 persists `{documents, docOrder}`. - `create.rs`: `create_index_from_path(.., previous)` reuse path; rows are moved (not copied) and reused rows are not re-normalised. - `cache_orchestrator.rs`: `load_previous_for_incremental` (fails closed on any structural inconsistency) + shared `manifest_compatible`. - `index.rs`: `files` manifest in `IndexManifest`/`CspIndex`, `from_path_with_previous`, `INDEX_SCHEMA_VERSION` 1 → 2, and `load_from_disk` rejects component count mismatches. - ADR-0005 records the per-file content hash (vs upstream `mtime_ns`) decision; `semble.md` and both READMEs updated. Refs #84 * fix(index): harden incremental reindex after review - `PreviousIndex::try_new`: sort manifest entries by `(start, count)` so a zero-chunk file that ties with the following file no longer fails the tiling check (which silently disabled incremental reuse for that tree). Regression test `zero_chunk_file_does_not_break_manifest_tiling`. - `Bm25Index::load`: rebuild postings from the persisted term counts via `insert_document` instead of materialising `freq` copies of every term; sum lengths in u64 and reject out-of-range counts. Drop the duplicate `Doc.chunk_id`. - `create_index_from_path`: embed all changed files' chunks in one batched pass (`dense::embed_chunk_refs`) so a cold build keeps the tokenizer's batch parallelism. - `FileManifestEntry::end()`: saturating add so a corrupt manifest fails the range checks instead of overflowing. - `load_previous_for_incremental`: reject a seed whose vector rows do not match the live model's dimension, falling back to a full rebuild. - `parse_manifest`: read `files` through the `FileManifestEntry` serde derive that `save` writes with. - Docs: query-term de-duplication is a real ranking divergence from upstream's query-frequency weighting, not rank-neutral; record it as an open parity gap in ADR-0005 and `semble.md`. Refs #84 * fix(index): skip files whose lossy display path collides with an indexed file On Unix, file names that differ only in invalid UTF-8 bytes collapse to the same `to_string_lossy` path. The BM25 chunk ids derived from that path would then collide and abort the whole build with "chunk_id already indexed". Keep the first such file, skip the rest with a warning, and add a Linux-only regression test (APFS rejects non-UTF-8 names). Refs #84 * chore: merge origin/main (#80 max_snippet_lines, #82 savings telemetry) into incremental reindexing * fix(index): reject zero BM25 term counts on load; take persisted vectors verbatim - Bm25Index::load rejects a zero term frequency (it would inflate the term's document frequency) so the cache falls back to a full rebuild. - SelectableBasicBackend::load no longer re-normalises rows that were normalised before save, keeping unchanged rows bit-identical across an incremental rebuild seeded from disk. Refs #84 * refactor(index): split create/sparse tests out, extract create_index_from_path helpers - create.rs / sparse.rs test modules move to create/tests.rs and sparse/tests.rs, matching the index/, dense/, cache_orchestrator/ layout. - create_index_from_path delegates to open_previous, display_path, take_previous_rows and embed_fresh_rows; behaviour unchanged. - load_previous_for_incremental compares the content selection as a set, so a duplicated request no longer matches a manifest that covers more. Refs #84 * perf(index): compare the cached backend dim instead of scanning every row Refs #84 * test(index): build the manifest key with the platform separator Refs #84



Summary
save_search_statswas defined and unit-tested but never called in any code path — nothing recorded to~/.csp/savings.jsonl, socsp savingsalways reported zero. This wires it in at the CLI/MCP app boundary and applies the semble#206 snippet accounting deferred from #75.Closes #81.
What changed
CspIndex.file_sizes(repo-relative path → UTF-16 char count) captured at build time infrom_path, carried throughfrom_git's re-root, and recomputed inload_from_diskwhen the source root is a still-present local directory (mirrors semble reading sizes offroot). Empty when source files aren't available (e.g. a cached git index) →file_charsis simply 0.save_search_statstakesmax_snippet_linesso recordedsnippet_charsreflects what the caller actually received (semble#206):None→ full chunk,Some(0)→ 0,Some(n)→ first n lines.search_output/find_related_output(CLI) andsearch_tool/find_related_tool(MCP), each behind an injectedOption<&Path>stats file.CspIndex::searchstays a pure, side-effect-free library call — telemetry lives at the app boundary (issue savings telemetry is not wired into the search flow (blocks semble#206) #81 permits either).Testability / no pollution
Every recording site takes an injected
Option<&Path>. The MCP server holds an injectablestats_filefield (Nonein tests); CLI dispatch is split intodispatch_with_stats(command, stats_file). Tests redirect telemetry to a temp file or disable it, so the developer's real~/.csp/savings.jsonlis never touched.Testing
cargo fmt --all && cargo clippy --all-targets --all-features -- -D warnings && cargo test --workspace— 273 lib + 22 CLI tests pass (4 network-gated ignored).Stacking
Stacked on #75 (
amondnet/max-snippet-lines) — base will retarget tomainonce #75 merges.🤖 Generated with Claude Code
Summary by cubic
Record token-savings telemetry for
searchandfind-relatedin the CLI and MCP socsp savingsshows real data, and addmax_snippet_linesto cap returned snippets.save_search_statsinto CLI and MCP behind an optional stats file; defaults to~/.csp/savings.jsonl.CspIndex.file_sizes(repo-relative path → UTF-16 char count), captured at build time, carried throughfrom_git, recomputed on load when the local root exists, and guarded against absolute, parent-escaping, or non-regular-file chunk paths.from_pathnow stores an absolute root so a relative-path build reloads its source from any cwd.save_search_statsnow takesmax_snippet_linessosnippet_charsreflects delivered lines (None = full, 0 = none, n = first n lines).{file_path, start_line, end_line, score, content?}; the nestedchunk,location, andlanguagefields are dropped. CLI defaults to full content, MCP defaults to 10 lines.CspIndex::searchside-effect-free; tests injectNoneor a temp file so the real stats file is untouched. Fixes savings telemetry is not wired into the search flow (blocks semble#206) #81.Written for commit c6652d0. Summary will update on new commits.